package org.seed.rpc.clientstub;

import lombok.extern.slf4j.Slf4j;
import org.seed.rpc.core.RpcService;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.ClassUtils;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.*;


@Slf4j
public class RpcServiceInterfaceScanner {


    private static final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
    private static final String RESOURCE_PATTERN = "/**/*.class";

    private static final String CONFIG_LOCATION_DELIMITERS = ",; \t\n";


    /**
     * 搜索全部的接口
     */
    public static Set<String> scanInterfaceSet(String basePackage) throws Exception {
        if (basePackage == null || basePackage.equals("")) {
            basePackage = "*";
        }
        return collectClassNameToSet(basePackage, RpcService.class, SearchMode.INTERFACE_ONLY);
    }

    /**
     * 扫描全部的实体类
     */
    public static Set<String> scanConcreteSet(String basePackage) throws Exception {
        if (basePackage == null || basePackage.equals("")) {
            basePackage = "*";
        }
        return collectClassNameToSet(basePackage, RpcService.class, SearchMode.CONCRETE_CLASS_ONLY);
    }

    private static Set<String> collectClassNameToSet(String basePackage, Class<? extends Annotation> annotationClass, SearchMode searchMode) throws IOException, ClassNotFoundException {
        String[] basePackages = tokenizeToStringArray(basePackage, CONFIG_LOCATION_DELIMITERS);
        List<TypeFilter> typeIncludes = new LinkedList<>();
        typeIncludes.add(new AssignableTypeFilter(annotationClass));
        typeIncludes.add(new AnnotationTypeFilter(annotationClass, false, true));
        Set<String> classSet = new HashSet<>();
        if (basePackages != null && basePackages.length > 0) {
            for (String pkg : basePackages) {
                String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(pkg) + RESOURCE_PATTERN;
                Resource[] resources = resourcePatternResolver.getResources(pattern);
                MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
                for (Resource resource : resources) {
                    if (resource.isReadable()) {
                        MetadataReader reader = readerFactory.getMetadataReader(resource);
                        if (matchesEntityTypeFilter(reader, readerFactory, typeIncludes)) {
                            ClassMetadata classMetadata = reader.getClassMetadata();
                            String className = classMetadata.getClassName();
                            switch (searchMode) {
                                case INTERFACE_ONLY:
                                    if (classMetadata.isInterface() && !classMetadata.isAnnotation()) {
                                        classSet.add(className);
                                    }
                                    break;
                                case ABSTRACT_CLASS_AND_INTERFACE:
                                    if (classMetadata.isInterface() || classMetadata.isAbstract()) {
                                        classSet.add(className);
                                    }
                                    break;
                                case CONCRETE_CLASS_ONLY:
                                    if (classMetadata.isConcrete()) {
                                        classSet.add(className);
                                    }
                                    break;
                                case CLASS_AND_INTERFACE:
                                default:
                                    classSet.add(className);
                            }

                            log.debug("found rpc annotation class: {}", className);
                        }
                    }
                }
            }
        }
        return classSet;
    }


    private static boolean matchesEntityTypeFilter(MetadataReader reader, MetadataReaderFactory readerFactory, List<TypeFilter> typeIncludes) throws IOException {
        for (TypeFilter filter : typeIncludes) {
            if (filter.match(reader, readerFactory)) {
                return true;
            }
        }
        return false;
    }


    private static String[] tokenizeToStringArray(String str, String delimiters) {
        return tokenizeToStringArray(str, delimiters, true, true);
    }


    private static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {
        if (str == null) {
            return null;
        } else {
            StringTokenizer st = new StringTokenizer(str, delimiters);
            ArrayList tokens = new ArrayList();

            while (true) {
                String token;
                do {
                    if (!st.hasMoreTokens()) {
                        return toStringArray((Collection) tokens);
                    }

                    token = st.nextToken();
                    if (trimTokens) {
                        token = token.trim();
                    }
                } while (ignoreEmptyTokens && token.length() <= 0);

                tokens.add(token);
            }
        }
    }

    private static String[] toStringArray(Collection<String> collection) {
        return collection == null ? null : (String[]) collection.toArray(new String[collection.size()]);
    }


    private enum SearchMode {

        INTERFACE_ONLY(0, "Interface Only"),
        ABSTRACT_CLASS_AND_INTERFACE(1, "Abstract Class And Interface"),
        CONCRETE_CLASS_ONLY(2, "Concrete Class Only"),
        CLASS_AND_INTERFACE(3, "Class And Interface"),
        ;

        private int code;
        private String title;


        SearchMode(int i, String s) {
            this.code = i;
            this.title = s;
        }
    }

}
